从零开始搭建 Vue 脚手架工具(二)

您所在的位置:网站首页 vue 脚手架命令 从零开始搭建 Vue 脚手架工具(二)

从零开始搭建 Vue 脚手架工具(二)

2022-05-06 07:57| 来源: 网络整理| 查看: 265

新的一年新的开始 github仓库地址

脚手架源码 ivue-cli

模板配置 webpack

开始

继续上一篇文章的讲解,让我们继续来看如何实现 init 功能。(如您想阅读上一篇内容可以点击这里)

新建一个脚手架的配置文件scaffold-config-dev.json

lib->scaffold->templates->scaffold-config-dev.json

{ "version": "0.1.0", "defaults": { "framework": "Vue", "template": "Basic" }, "frameworks": [ { "value": "Vue", "name": "Vue2", "subList": { "template": [ { "value": "Basic", "name": "Basic", "git": "https://github.com/lavas-project/lavas-template-vue.git", "branch": "release-basic", "desc": "基础模版,包含 Ivue Material Ui", "locals": { "zh_CN": { "desc": "基础模版,包含 Ivue Material Ui \n包含额外配置选项 (默认包含 Babel)" }, "en": { "desc": "Basic Template, contains Ivue Material Ui \nIncludes additional configuration options (default Babel)" } } }, { "value": "Basic-MPA", "name": "Basic-MPA", "git": "https://github.com/lavas-project/lavas-template-vue.git", "branch": "release-basic-mpa", "desc": "多页面模版,包含 Ivue Material Ui 和 PWA 工程化相关必需内容", "locals": { "zh_CN": { "desc": "多页面模版,包含 Ivue Material Ui \n(默认包含 Babel, Router,Sass)" }, "en": { "desc": "Mpa Template, contains Ivue Material Ui \n(default Babel,Router,Sass)" } } }, { "value": "PWA-SPA", "name": "PWA-SPA", "git": "https://github.com/lavas-project/lavas-template-vue.git", "branch": "release-pwa-spa", "desc": "PWA 单页面模版,包含 Ivue Material Ui 和 PWA 工程化相关必需内容", "locals": { "zh_CN": { "desc": "PWA 单页面模版,包含 Ivue Material Ui 和 PWA 工程化相关必需内容 \n(默认包含 Babel,Router,Sass)" }, "en": { "desc": "PWA Basic Template, contains Ivue Material Ui and PWA \n(default Babel,Router,Sass)" } } }, { "value": "PWA-MPA", "name": "PWA-MPA", "git": "https://github.com/lavas-project/lavas-template-vue.git", "branch": "release-pwa-mpa", "desc": "PWA 多页面模版,包含 Ivue Material Ui 和 PWA 工程化相关必需内容", "locals": { "zh_CN": { "desc": "PWA 多页面模版,包含 Ivue Material Ui 和 PWA 工程化相关必需内容 \n(默认包含 Babel,Router,Vuex,Sass)" }, "en": { "desc": "PWA Mpa Template, contains Ivue Material Ui and PWA \n(default Babel,Router,Vuex,Sass)" } } } ] } } ], "schema": { "framework": { "type": "list", "name": "前端框架", "description": "项目所选择的基础框架", "locals": { "zh_CN": { "name": "前端框架", "description": "项目所选择的基础框架" }, "en": { "name": "framework", "description": "The framework chosen for the project" } }, "required": true, "link": "frameworks", "default": "vue", "checkbox": false, "disable": true, "depLevel": 0, "list": [], "jsonType": "string" }, "template": { "type": "list", "name": "模版类型", "description": "初始化项目时选中的模版类型", "locals": { "zh_CN": { "name": "模版类型", "description": "初始化项目时选中的模版类型" }, "en": { "name": "template", "description": "The type of template selected when initializing the project" } }, "dependence": "framework", "default": "Basic", "ref": "template", "depLevel": 1, "checkbox": false, "required": true, "list": [], "jsonType": "string" }, "checkbox": { "type": "checkbox", "key": "checkbox", "name": "选择选项", "description": "检查项目所需的功能", "required": true, "checkbox": true, "list": [ { "value": "router", "name": "Router", "checked": false }, { "value": "vuex", "name": "Vuex", "checked": false }, { "value": "css", "name": "CSS Pre-processors", "checked": false }, { "value": "typescript", "name": "Typescript", "checked": false } ], "depLevel": 0, "jsonType": "string" }, "csssProcessors": { "type": "list", "key": "csssProcessors", "name": "选择CSS预处理器", "description": "(支持PostCSS,Autoprefixer和CSS模块默认情况下)", "required": true, "checkbox": true, "list": [ { "value": "scss", "name": "Sass/SCSS" }, { "value": "less", "name": "Less" }, { "value": "stylus", "name": "Stylus" } ], "depLevel": 0, "jsonType": "string" } } } 创建 init 命令

commander 下新建文件在该目录下管理主逻辑代码

commander->scaffold->index.js

'use strict'; // init 安装脚手架命令 const init = require('./action'); // 提示文件 const locals = require('../../locals')(); module.exports = function (program) { // define init command program .command('init') .description(locals.INIT_DESC) .option('-f, --force', locals.INIT_OPTION_FORCE) .action(options => init({ force: options.force })); };

locals.js 文件中添加提示

module.exports = { ..... INIT_DESC: '初始化 ivue-cli 项目', INIT_OPTION_FORCE: '是否覆盖已有项目', ..... };

以上创建了 init 命令的运行

init 命令的代码实现

首先检查当前网络环境, 创建检查网络环境方法isNetworkConnect

lib->utils->index.js

const dns = require('dns'); /** * 检测当前网络环境 * * @return {Boolean} 是否联网 */ exports.isNetworkConnect = function () { return new Promise((reslove) => { dns.lookup('baidu.com', (err) => reslove(!(err && err.code === 'ENOTFOUND'))); }); }

创建一个文件管理错误提示

locals->zh_CN->index.js

module.exports = { ..... NETWORK_DISCONNECT: '创建工程需要下载云端模版', NETWORK_DISCONNECT_SUG: '请确认您的设备处于网络可访问的环境中', WELECOME: `欢迎使用`, GREETING_GUIDE: '开始新建一个项目', ..... };

新建 action.js用于init命令核心代码文件,同时引用 isNetworkConnect 检测网络

commander->scaffold->action.js

const utils = require('../../lib/utils') const log = require('../../lib/utils/log'); const locals = require('../../locals')(); module.exports = async function (conf) { // 检测当前网络环境 let isNetWorkOk = await utils.isNetworkConnect(); // 离线提示 if (!isNetWorkOk) { log.error(locals.NETWORK_DISCONNECT); log.error(locals.NETWORK_DISCONNECT_SUG); return; } log.info(locals.WELECOME); log.info(locals.GREETING_GUIDE + '\n'); ..... }

当没有网络时会输出以下内容:

否则输出如下内容:

初始化过程的6个步骤

现在开始让我们来看看初始化过程的初始化过程的6个步骤

第一步:从云端配置获取 Meta 配置。确定将要下载的框架和模板 lish

1.添加提示

locals->zh_CN->index.js

module.exports = { ..... LOADING_FROM_CLOUD: '正在拉取云端数据,请稍候', ..... };

2.安装包

// 下载中动画效果 "ora": "^1.3.0"

使用后效果如图:

3.引用下载配置的方法 getMetaSchema,下载完成后调用spinner.stop() 停止下载中效果

commander->scaffold->action.js

const scaffold = require('../../lib/scaffold'); // 第一步:从云端配置获取 Meta 配置。确定将要下载的框架和模板 lish let spinner = ora(locals.LOADING_FROM_CLOUD + '...'); spinner.start(); let metaSchema = await scaffold.getMetaSchema(); spinner.stop();

4.getMetaSchema()方法实现, 新建一个store.js用于缓存数据

lib/scaffold

/** * @file 简单的 store */ 'use strict'; const store = {}; module.exports = { /** * setter * * @param {String} name store key * @param {Any} value store value */ set (name, value) { store[name] = value; }, /** * getter * * @param {String} name store key * @return {[type]} store value */ get (name) { return store[name]; } }

设置公共配置,新建一个config.js

/** * @file scaffold 相关配置 */ 'use strict'; const jsonP = require('./templates/scaffold-config-dev.json'); module.exports = { /** * 全局的配置文件地址 * * @type {String} */ GLOBAL_CONF_URL: { production: jsonP, development: jsonP }, }

获取meta配置,新建getMeta.js

const store = require('./store'); const conf = require('./config'); // 如果是开发环境就使用开发环境的 CONF 数据,避免污染线上的 CONF 数据 const confUrl = conf.GLOBAL_CONF_URL[ process.env.NODE_ENV === 'development' ? 'development' : 'production' ]; /** * 请求全局的配置 JOSN 数据 * * @return {Object} JSON 数据 */ module.exports = async function () { let data = store.get('data'); // 如果 store 中已经存在了,2s 后再尝试更新下是不是有最新的数据 if (data) { let timer = setTimeout(async () => { let json = await confUrl; store.set('data', json); clearTimeout(timer); }, 2000); return data; } // 如果 store 里面没有,我们马上就获取一份最新的数据 data = await confUrl; store.set('data', data); return data; }

以上新建了获取配置的方法接下来

在 lib/scaffold中新建文件schema.js,获取meta配置项

const getMeta = require('./getMeta'); /** * 获取元 Schema, 即模板选择的 Schema * * @return {Object} 元 Schema */ exports.getMetaSchema = async function () { // 获取整个配置文件 scaffold-config-dev.json let meta = await getMeta(); .... }

我们还需要去获 scaffold-config-dev.json 中的 schema字段的内容所以我们需要 新建parseConfToSchema方法整理schema字段

/** * 把约定的 JSON CONF 内容解析成可自动化处理的 schema * * @param {Object} conf 按照约定格式的配置 json 文件 * @return {Object} schema */ function parseConfToSchema (conf = {}) { let properties = conf.schema || {}; Object.keys(properties).forEach((key) => { let item = properties[key]; if (item.type === 'list') { if (item.link && !item.dependence) { properties[key].list = conf[item.link]; } else if (item.dependence) { properties[item.dependence].list.forEach((depItem) => { if (depItem.value === conf.defaults[item.dependence]) { properties[key].list = depItem.subList ? (depItem.subList[key] || []) : []; } }); } } }); return properties; }

新建parseConfToSchema后,引用parseConfToSchema方法获取schema字段里的配置,并且把配置保存到store里面缓存起来减少请求次数

/** * 获取元 Schema, 即模板选择的 Schema * * @return {Object} 元 Schema */ exports.getMetaSchema = async function () { // 获取整个配置文件 scaffold-config-dev.json let meta = await getMeta(); // 获取配置文件 scaffold-config-dev.json 的 schema let metaSchema = parseConfToSchema(meta); store.set('metaSchema', metaSchema); return metaSchema; }

5.到这来我们已经完成了meta配置的获取了,然后我们需要把方法暴露给index.js进行代码管理

index.js

const store = require('./store'); /** * 获取元 Schema - 涉及模版下载的 Schema * * @return {Promise} Meta Schema */ exports.getMetaSchema = async function () { return store.get('metaSchema') || await Schema.getMetaSchema(); }

现在让我们运行init看看输出的内容

// 第一步:从云端配置获取 Meta 配置。确定将要下载的框架和模板 lish let spinner = ora(locals.LOADING_FROM_CLOUD + '...'); spinner.start(); let metaSchema = await scaffold.getMetaSchema(); spinner.stop(); console.log(metaSchema)

以上就是获取到的模板配置内容

第二步:等待用户选择将要下载的框架和模板

到了这一步是我们需要让用户选择哪个模板的时候了

我们需要有一个表单让用户去选择

commander->scaffold->action.js

const formQ = require('./formQuestion'); // 第二步:等待用户选择将要下载的框架和模板 let metaParams = await formQ(metaSchema);

添加提示

locals->zh_CN->index.js

module.exports = { ..... INPUT_INVALID: '输入不符合规范', PLEASE_INPUT: '请输入', PLEASE_INPUT_NUM_DESC: '请选择一个数字指定', PLEASE_INPUT_NUM: '请输入数字', PLEASE_INPUT_RIGHR_NUM: '请输入正确的数字', PLEASE_SELECT: '请选择一个', PLEASE_SELECT_DESC: '按上下键选择', ..... };

安装需要的包

// 将node.js现代化为当前ECMAScript规范 "mz": "^2.7.0", // node fs 方法添加promise支持 "fs-extra": "^4.0.1", // 常见的交互式命令行用户界面的集合 "inquirer": "^6.2.0",

1.首先新建formQuestion.js用于用户表单选择

新建公共方法questionInput、questionYesOrNo、questionList、questionCheckboxPlus、getGitInfo

commander->scaffold->formQuestion.js

const exec = require('mz/child_process').exec; const fs = require('fs-extra'); const os = require('os'); const inquirer = require('inquirer'); const path = require('path'); const locals = require('../../locals')(); const log = require('../../lib/utils/log'); 'use strict'; /** * 获取当前用户的 git 账号信息 * * @return {Promise} promise 对象 */ async function getGitInfo () { let author; let email; try { // 尝试从 git 配置中获取 author = await exec('git config --get user.name'); email = await exec('git config --get user.email'); } catch (e) { } author = author && author[0] && author[0].toString().trim(); email = email && email[0] && email[0].toString().trim(); return { author, email }; } /** * 询问 input 类型的参数 * * @param {string} key 参数的 key * @param {Object} schema schema 内容 * @param {Object} params 当前已有的参数 * @return {Object} question 需要的参数 */ async function questionInput (key, schema, params) { let con = schema[key]; let { name, invalidate } = con; let defaultVal = con.default; // 语言 locals - zh_CN let itemLocals = con.locals && con.locals[locals.LANG]; if (itemLocals) { // locals - zh_CN - name name = itemLocals.name || name; // 模板类型 defaultVal = itemLocals.default || defaultVal; invalidate = itemLocals.invalidate || invalidate; } con.validate = () => !!1; // 如果输入项是 author 或者 email 的,尝试去 git config 中拿默认内容 if (key === 'author' || key === 'email') { let userInfo = await getGitInfo(); defaultVal = userInfo[key] || con.default; } if (key === 'dirPath') { defaultVal = path.resolve(process.cwd(), con.default || ''); con.validate = value => { let nowPath = path.resolve(process.cwd(), value || ''); if (!fs.existsSync(nowPath)) { return invalidate || locals.INPUT_INVALID; } else { } return true; } } // 匹配输入是否符合规范 if (con.regExp) { let reg = new RegExp(con.regExp); con.validate = value => { if (!reg.test(value)) { return invalidate || locals.INPUT_INVALID; } return true; } } return { // 密码 'type': con.type === 'password' ? 'password' : 'input', 'name': key, // 提示信息 'message': `${locals.PLEASE_INPUT}${name}: `, // 默认值 'default': defaultVal, // 验证 'validate': con.validate } } /** * 询问 boolean 类型的参数 * * @param {string} key 参数的 key * @param {Object} schema schema 内容 * @param {Object} params 当前已有的参数 * @return {Object} question 需要的参数 */ async function questionYesOrNo (key, schema, params) { let con = schema[key]; // 名称 let name = con.name; // 语言 let itemLocals = con.locals && con.locals[locals.LANG]; // 获取相应语言的提示 if (itemLocals) { name = itemLocals.name || name; } return { 'type': 'confirm', 'name': key, 'default': false, 'message': `${name}? :` } } /** * 询问 list 类型的参数 (多选或者单选) * * @param {string} key 参数的 key * @param {Object} schema schema 内容 * @param {Object} params 当前已有的参数 * @return {Object} question 需要的参数 */ function questionList (key, schema, params) { let con = schema[key]; // 来源列表 let sourceLish = []; // 选择列表 let choiceList = []; let text = ''; let valueList = []; let listName = con.name; // 模板类型 let listLocals = con.locals && con.locals[locals.LANG]; // 获取相应语言的提示 if (listLocals) { listName = listLocals.name; } // 依赖 if (!con.dependence) { sourceLish = con.list; } // 层级 else if (con.depLevel > 0) { // 表示是级联的操作 let dependence = con.dependence; // 类型 template let ref = con.ref; let depList = schema[dependence].list; let depValue = params[dependence] || schema[dependence].list[0]; depList.forEach((depItem) => { if (depItem.value === depValue) { sourceLish = (depItem.subList && depItem.subList[ref]); } }); } sourceLish.forEach((item, index) => { let url = ''; let { desc, name } = item; let itemLocals = item.locals && item.locals[locals.LANG]; // 相应语言的提示 if (itemLocals) { desc = itemLocals.desc || desc; name = itemLocals.name || name; } desc = log.chalk.gray('\n ' + desc); choiceList.push({ value: item.value, name: `${name}${desc}${url}`, short: item.value }); valueList.push(item.value); text += '' + log.chalk.blue('\n [' + log.chalk.yellow(index + 1) + '] ' + name) + desc; }); // 如果是 windows 下的 git bash 环境,由于没有交互 GUI,所以就采用文本输入的方式来解决 if (os.platform() === 'win32' && process.env.ORIGINAL_PATH) { return { 'type': 'input', 'name': key, 'message': locals.PLEASE_INPUT_NUM_DESC + ' ' + listName + ':' + text + '\n' + log.chalk.green('?') + ' ' + locals.PLEASE_INPUT_NUM + ':', 'default': 1, 'valueList': valueList, // 验证 'validate' () { if (!/\d+/.test(value) || +value > valueList.length || +value { let { name } = item; let itemLocals = item.locals && item.locals[locals.LANG]; if (itemLocals) { name = itemLocals.name || name; } choiceList.push({ value: item.value, name: name, checked: item.checked }); }); return { 'type': con.type, 'name': key, 'message': con.name, 'choices': choiceList } }

2.公共方法新建完成后,让我们开始编写表单代码

commander->scaffold->formQuestion.js

/** * 解析schme, 生成 form 表单 * * @param {Object} schema 传入的 schema 规则 * @return {Object} 获取的 form 参数 */ module.exports = async function (schema) { let params = {}; // 只有basic模板才可以进行配置定制 if (schema.key) { let opts = {}; let data = {}; // 配置选择,复选框 opts = await questionCheckboxPlus(schema.key, { [schema.key]: schema }, params); // 输出选择的配置 data = await inquirer.prompt([opts]).then(function (answers) { return { [schema.key]: answers[schema.key] }; }); params = Object.assign({}, params, data); return params } else { for (let key of Object.keys(schema)) { let con = schema[key]; let type = con.type; let opts = {}; let data = {}; switch (type) { case 'string': case 'number': case 'password': // 输入密码 opts = await questionInput(key, schema, params); break; case 'boolean': // 确认 opts = await questionYesOrNo(key, schema, params); break; case 'list': // 列表 opts = await questionList(key, schema, params); break; } // 如果 list 只有一个 item 的时候,就不需要用户选择了,直接给定当前的值就行 if (type === 'list' && con.list.length === 1) { data[key] = con.list[0].value; } else if (!con.disable && !con.key) { data = await inquirer.prompt([opts]); if (opts.valueList) { data[key] = opts.valueList[+data[key] - 1]; } } params = Object.assign({}, params, data); } } return params; };

由于我们可以对Basic模板进行配置的定制所以现在需要修改commander->scaffold->action.js里面的代码

const formQ = require('./formQuestion'); // 第二步:等待用户选择将要下载的框架和模板 let metaParams = await formQ(metaSchema); let checkboxParams; let cssParams; // 只有基础模板才可以自定义选项 if (metaParams.template === 'Basic') { // 获取用户选择的参数 checkboxParams = await formQ(metaSchema.checkbox); // 是否选择了css if (checkboxParams.checkbox.indexOf('css') > -1) { cssParams = await formQ(metaSchema.csssProcessors); } }

修改后如上

到了这里再让我们运行init看看输出的是什么

选择basic模板后返回可配置的选项

第三步:通过用户选择的框架和模板,下载模板

这一步是让脚手架去下载用户选择的模板

添加提示

locals->zh_CN->index.js

module.exports = { ...... META_TEMPLATE_ERROR: '获取模版 Meta 信息出错', DOWNLOAD_TEMPLATE_ERROR: '下载模版出错,请检查当前网络', ...... };

安装需要的包

"lodash": "^4.17.4", "ajv": "^5.1.3", "axios": "^0.17.1" "compressing": "^1.3.1"

1.新建download方法,返回模板的 Schema 信息

lib->scaffold->index.js

/** * 通过指定的 meta 参数下载模版,下载成功后返回模板的 Schema 信息 * * @param {Object} metaParams 导出参数 * @return {*} 下载的临时路径 或者 报错对象 */ exports.download = async function (metaParams = {}) { }

2.新建导出所有文件路径的方法extendsDefaultFields去导出所有的文件

lib->scaffold->index.js

const store = require('./store'); const Schema = require('./schema'); const path = require('path'); const _ = require('lodash'); /** * 获取导出的所有的 fields (包含 default 参数) * * @param {Object} fields 传入的 fields * @param {Obejct} templateConf 模版的配置 * @return {Object} 输出的 fields */ async function extendsDefaultFields (fields = {}, templateConf = {}) { let defaultFields = {}; let schema = store.get('schema') || await Schema.getSchema(templateConf) Object.keys(schema).forEach((key) => (defaultFields[key] = schema[key].default)) /* eslint-disable fecs-use-computed-property */ // defaultFields.name = fields.name || 'ivue-cli' defaultFields.name = fields.name || 'ivue-cli'; defaultFields.dirPath = path.resolve(process.cwd(), fields.dirPath || '', defaultFields.name); return _.merge({}, defaultFields, fields); }

3.然后我们在schema.js中增加getSchema方法用于生成用户输入的表单

lib->scaffold->schema.js

/** * 获取 Schema, 用于生成用户输入的表单 * * @param {Object} templateConf 每个模版的 config * @return {Object} 返回的 JSON Schema */ exports.getSchema = function (templateConf = {}) { return parseConfToSchema(templateConf); }

4.然后回到lib->scaffold->index.js文件创建download方法下载成功后返回模板的 schema字段的信息

lib->scaffold->index.js

/** * 通过指定的 meta 参数下载模版,下载成功后返回模板的 Schema 信息 * * @param {Object} metaParams 导出参数 * @return {*} 下载的临时路径 或者 报错对象 */ exports.download = async function (metaParams = {}) { // 输出导出路径相关配置 metaParams = await extendsDefaultFields(metaParams); }

到了这里我们看看metaParams输出的是什么

以下输出包含了我们需要在哪里创建文件的路径,和模板名称、类型

5.接下来我们去创建真正的 download 方法下载一个指定的模版

新建文件lib->scaffold->template.js,创建下载指定的模版方法

lib->scaffold->template.js

/** * 下载一个指定的模版 * * @param {Object} metaParams 导出模版所需字段, 从 mataSchema 中得出 * @return {Objecy} 导出的结果 */ exports.download = async function (metaParams = {}) { }

先来获取模板的信息,创建getTemplateInfo方法获取模版信息

lib->scaffold->template.js

const getMeta = require('./getMeta'); const store = require('./store'); /** * 获取模版信息 * * @param {Object} metaParam 元参数 * @return {Object} framework 和 template 信息 */ async function getTemplateInfo (metaParam) { try { // 获取全部配置 let meta = await getMeta(); let frameworkValue = metaParam.framework || meta.defaults.framework || 'vue'; let templateValue = metaParam.template || meta.defaults.template || 'template' // 对应的模板信息 let framework = meta.frameworks.filter(item => item.value === frameworkValue)[0]; // 仓库地址等信息 let template = framework.subList.template.filter(item => item.value === templateValue)[0]; // 版本号 let version = meta.version; store.set('framework', framework); store.set('template', template); store.set('version', version); return { framework, template, version }; } catch (e) { // 如果这一步出错了,只能说明是 BOS 上的 Meta 配置格式错误。。 throw new Error(locals.META_TEMPLATE_ERROR); } }

回到download方法中添加getTemplateInfo方法输出模板详细信息

lib->scaffold->template.js

/** * 下载一个指定的模版 * * @param {Object} metaParams 导出模版所需字段, 从 mataSchema 中得出 * @return {Objecy} 导出的结果 */ exports.download = async function (metaParams = {}) { let { framework, template, version } = await getTemplateInfo(metaParams); }

接下来我们来看看getTemplateInfo方法输出的信息:

输出包含了模板的仓库地址,描述,等信息

6.设置从云端下载到本地的路径

在download方法中添加如下代码

lib->scaffold->template.js

const conf = require('./config'); const path = require('path'); /** * 下载一个指定的模版 * * @param {Object} metaParams 导出模版所需字段, 从 mataSchema 中得出 * @return {Objecy} 导出的结果 */ exports.download = async function (metaParams = {}) { let { framework, template, version } = await getTemplateInfo(metaParams); // 下载到本地的路径 let storeDir = path.resolve( conf.LOCAL_TEMPLATES_DIR, framework.value, template.value + '_' + version ) }

其中conf.LOCAL_TEMPLATES_DIR 为本地模版存放路径

lib->scaffold->config.js

const path = require('path'); const utils = require('../utils'); module.exports = { /** * 本地模版存放路径 * * @type {String} */ LOCAL_TEMPLATES_DIR: path.resolve(utils.getHome(), 'tmp'), ..... }

utils.getHome() 为获取云端仓库的跟目录

lib->utils->index.js

const os = require('os'); const path = require('path'); const fs = require('fs-extra'); /** * 获取项目根目录 * * @return {string} 目录 Path */ exports.getHome = function () { let dir = process.env[ os.platform() === 'win32' ? 'APPDATA' : 'HOME' ] + path.sep + '.ivue-project' // 如果这个目录不存在,则创建这个目录 !fs.existsSync(dir) && fs.mkdirSync(dir); return dir; };

7.接下来我们需要去验证用户的输入是否与配置中的验证规则相匹配

在schema.js 中添加getMetaJsonSchema方法

lib->scaffold->schema.js

/** * 获取 meta JSON Schema, 用于验证 json 表单 * * @return {Object} 返回的 JSON Schema */ exports.getMetaJsonSchema = async function () { let meta = await getMeta(); let metaSchema = parseConfToSchema(meta); store.set('metaSchema', metaSchema); return metaSchema; }

回到download方法中添加验证用户输入

lib->scaffold->template.js

/** * 下载一个指定的模版 * * @param {Object} metaParams 导出模版所需字段, 从 mataSchema 中得出 * @return {Objecy} 导出的结果 */ exports.download = async function (metaParams = {}) { let { framework, template, version } = await getTemplateInfo(metaParams); let storeDir = path.resolve( conf.LOCAL_TEMPLATES_DIR, framework.value, template.value + '_' + version ) // 验证是否是json字符串 let ajv = new Ajv({ allErrors: true }); let metaJsonSchema = store.get('metaJsonSchema') || await schema.getMetaJsonSchema(); // 验证用户输入 let validate = ajv.compile(metaJsonSchema); let valid = validate(metaParams); if (!valid) { throw new Error(JSON.stringify(validate.errors)); } }

8.验证通过后我们开始从服务器上拉取模板

新增downloadTemplateFromCloud方法用于下载模板,从服务器上拉取模版

lib->scaffold->template.js

/** * 通过指定框架名和模版名从服务器上拉取模版(要求在模版 relase 的时候注意上传的 CDN 路径) * * @param {string} framework 框架名称 * @param {string} template 模版名称 * @param {string} targetPath 模版下载后存放路径 */ async function downloadTemplateFromCloud (framework, template, targetPath) { const outputFilename = path.resolve(targetPath, 'template.zip'); // existsSync: 如果路径存在,则返回 true,否则返回 false。 // removeSync 删除文件、目录 fs.existsSync(targetPath) && fs.removeSync(targetPath); // 确保目录存在。如果目录结构不存在,则创建它 fs.mkdirsSync(targetPath); framework = (framework || 'vue').toLowerCase(); template = (template || 'basic').toLowerCase().replace(/\s/, '-'); try { // 请求模板 let result = await axios.request({ responseType: 'arraybuffer', url: 'https://codeload.github.com/qq282126990/webpack/zip/release-' + template, method: 'get', headers: { 'Content-Type': 'application/zip' } }); fs.writeFileSync(outputFilename, result.data); // 解压缩是反响过程,接口都统一为 uncompress await compressing.zip.uncompress(outputFilename, targetPath); fs.removeSync(outputFilename); } catch (e) { throw new Error(locals.DOWNLOAD_TEMPLATE_ERROR); }

回到download方法,添加方法downloadTemplateFromCloud通过指定框架名和模版名从服务器上拉取模版,模板内容将下载到storeDir路径下

lib->scaffold->template.js

...... // 通过指定框架名和模版名从服务器上拉取模版 await downloadTemplateFromCloud(framework.value, template.value, storeDir); .....

下载后如图:

9.接下来我们去获取模板下载后的meta.json文件

download方法中添加代码,获取文件夹名称以及下载后的meta.json的内容

lib->scaffold->template.js

...... // 获取文件夹名称 const files = fs.readdirSync(storeDir); store.set('storeDir', `${storeDir}/${files}`); let templateConfigContent = fs.readFileSync(path.resolve(`${storeDir}/${files}`, 'meta.json'), 'utf-8'); let templateConfig = JSON.parse(templateConfigContent); store.set('templateConfig', templateConfig); return templateConfig; ......

10.然后我们需要吧代码统一管理到index.js中

lib->scaffold->index.js

const template = require('./template'); /** * 通过指定的 meta 参数下载模版,下载成功后返回模板的 Schema 信息 * * @param {Object} metaParams 导出参数 * @return {*} 下载的临时路径 或者 报错对象 */ exports.download = async function (metaParams = {}) { metaParams = await extendsDefaultFields(metaParams); return await template.download(metaParams); }

11.最后我们导出download方法

新增通过用户选择的框架和模板,下载模板的代码

commander->scaffold->action.js

// 第三步:通过用户选择的框架和模板,下载模板 spinner.start(); let templateConf = await scaffold.download(metaParams, checkboxParams); spinner.stop();

12.由于我们可以对基础模本进行自定义选项所以还需要增加以下代码来下载对应的选项配置

新增包

// ETPL是一个强复用,灵活,高性能的JavaScript的模板引擎,适用于浏览器端或节点环境中视图的生成 "etpl": "^3.2.0"

在lib->scaffold->index.js中新增方法setMainJs设置webpack模板的main.js文件,setCheckboxParams通过指定的参数渲染下载成功的模板,setCssParams配置css包

由于customize文件夹内容过多此处不进行展示,详情可以查看这里

lib->scaffold->index.js

const etpl = require('etpl'); const fs = require('fs-extra'); const path = require('path'); // 设置router 配置 const routerConfig = require('../../../../customize/router'); // 设置 vuex 配置 const vuexConfig = require('../../../../customize/vuex'); // 设置 typescriptConfig 配置 const typescriptConfig = require('../../../../customize/typescript'); /** * main.js * * @param {String} storeDir 文件根目录 * @param {String} currentDir 当前文件目录 * @param {Function} etplCompile 字符串转换 * @param {Array} params 需要设置的参数 */ function setMainJs (storeDir, currentDir, etplCompile, params) { // 模块 let nodeModules = ''; // 路径列表 let urls = ''; // 配置 let configs = ''; // 名字列表 let names = ''; params.forEach((key) => { // 插入路由配置 if (key === 'router') { nodeModules += `${nodeModules.length === 0 ? '' : '\n'}import VueRouter from 'vue-router'${nodeModules.length === 0 ? '\n' : ''}`; urls += `${urls.length === 0 ? '' : '\n'}import router from './router'`; configs += `\nVue.use(VueRouter)`; names += `${names.length === 0 ? '' : '\n'} router,`; } // 插入vuex配置 if (key === 'vuex') { urls += `${urls.length === 0 ? '' : '\n'}import store from './store'`; names += `${names.length === 0 ? '' : '\n'} store,`; } }); // main.js let mainJs = `import Vue from 'vue' ${nodeModules} import App from './App.vue' ${urls}${urls.length > 0 ? '\n' : ''} import IvueMaterial from 'ivue-material' import 'ivue-material/dist/styles/ivue.css' ${configs} Vue.use(IvueMaterial) Vue.config.productionTip = false new Vue({ ${names}${names.length > 0 ? '\n' : ''} render: h => h(App), }).$mount('#app') `; mainJs = etplCompile.compile(mainJs)(); let name if (params.indexOf('typescript') > -1) { name = 'main.ts'; } else { name = 'main.js'; } // 重新写入文件 fs.writeFileSync(path.resolve(`${storeDir}/src`, name), mainJs); } /** * 通过指定的参数渲染下载成功的模板 * * @param {Array} params 需要设置的参数 */ exports.setCheckboxParams = async function (params = []) { const storeDir = store.get('storeDir'); const templateConfig = store.get('templateConfig'); const etplCompile = new etpl.Engine(templateConfig.etpl); const currentDir = './packages/customize/router/code' params.forEach((key) => { // 插入路由配置 if (key === 'router') { routerConfig.setFile(storeDir, etplCompile,params); } // 插入 vuex 配置 if (key === 'vuex') { vuexConfig.setFile(storeDir, etplCompile, params); } // 插入 typescript 配置 if (key === 'typescript') { typescriptConfig.setFile(storeDir, etplCompile); } }); // 修改 main.js setMainJs(storeDir, currentDir, etplCompile, params); // 设置 shims-vue.d.ts if (params.indexOf('typescript') > -1) { setShimsVueDTs(storeDir, currentDir, etplCompile, params); } } /** * 配置css参数 * * @param {Array} params 需要设置的参数 */ exports.setCssParams = async function (params = '') { const storeDir = store.get('storeDir'); const templateConfig = store.get('templateConfig'); const etplCompile = new etpl.Engine(templateConfig.etpl); let nodeModules = {}; // scss if (params === 'scss') { nodeModules = { 'node-sass': '^4.12.0', 'sass-loader': '^7.2.0' }; } // less else if (params === 'less') { nodeModules = { 'less': '^3.0.4', 'less-loader': '^7.2.0' }; } // stylus else if (params === 'stylus') { nodeModules = { 'stylus': '^0.54.5', 'stylus-loader': '^3.0.2' }; } // 设置css版本号 setCssPackConfig(storeDir, etplCompile, nodeModules); }

最后在 commander->scaffold->action.js 中添加如下代码设置用户选择的参数:

commander->scaffold->action.js

module.exports = async function (conf) { ...... // 设置用户选择的参数 // 只有基础模板才可以自定义选项 if (metaParams.template === 'Basic') { await scaffold.setCheckboxParams(checkboxParams.checkbox); // 是否选择了css if (cssParams) { await scaffold.setCssParams(cssParams.csssProcessors); } } ...... } 第四步:根据下载的模板的 meta.json 获取当前模板所需要用户输入的字段 schema

模板下载完成后我们需要用户对模板的字段进行设置,如设置作者名称、作者邮箱、项目描述、项目名称。所以我们需要获取模板中的meta.json 文件。知道那些字段是需要用户去设置的。

1.新增getSchema方法,获取meta.json配置

lib->scaffold->index.js

/** * 获取 Schema - 涉及模版渲染的 Schema * * @param {Object} templateConf 模版自己的配置 * @return {Promise} Schema */ exports.getSchema = async function (templateConf = {}) { if (!templateConf) { // 如果实在没有提前下载模板,就现用默认的参数下载一个 templateConf = await Schema.download(); } return Schema.getSchema(templateConf); }

2.然后在 commander->scaffold->action.js 添加一下代码,输出获取到的配置

commander->scaffold->action.js

module.exports = async function (conf) { ...... // 第四步:根据下载的模板的 meta.json 获取当前模板所需要用户输入的字段 schema let schema = await scaffold.getSchema(templateConf); ...... } 第五步:等待用户输入 schema 所预设的字段信息

到了这里我们需要用上一步的配置去让用户进行输入

commander->scaffold->action.js

// 第五步:等待用户输入 schema 所预设的字段信息 let params = await formQ(schema);

我们再次运行init看看输出的是什么

如上输出使用户可以自定义自己的package.json

输入完成后如图:

第六步:渲染模板,并导出到指定的文件夹(当前文件夹)

新增提示

LOADING_EXPORT_PROJECT: '正在导出工程', INIT_SUCCESS: '项目已创建成功', INIT_NEXT_GUIDE: '您可以操作如下命令快速开始开发工程', RENDER_TEMPLATE_ERROR: '模板渲染出错',

安装包

"glob": "^7.1.2", "archiver": "^1.3.0"

config.js 新增配置

lib->scaffold->config.js

/** * 渲染模版时默认忽略的文件或文件夹 * * @type {Arrag} */ DEFAULT_RENDER_IGNORES: [ 'node_modules', '**/*.tmp', '**/*.log', '**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.bmp', '**/*.gif', '**/*.ico', '**/*.svg', '**/*.woff', '**/*.ttf', '**/*.woff2' ], /** * 默认的 etpl 配置 * * @type {Object} */ ETPL: { commandOpen: '{%', commandClose: '%}', variableOpen: '*__', variableClose: '__*' }, /** * render common data 渲染时间 * * @type {Object} */ COMMON_DATA: { year: (new Date()).getFullYear(), time: Date.now() }, /** * 导出时默认忽略的文件或文件夹 * * @type {Array} */ DEFAULT_EXPORTS_IGNORES: [ '.git', 'meta.js', 'meta.json' ],

1.我们先在lib->scaffold->template.js中新增方法renderTemplate渲染template 里面的所有文件

lib->scaffold->template.js

const conf = require('./config'); // ETPL是一个强复用,灵活,高性能的JavaScript的模板引擎,适用于浏览器端或节点环境中视图的生成 const etpl = require('etpl'); // Match files using the patterns the shell uses, like stars and stuff. const glob = require('glob'); // 用于存档生成的流式界面 const archiver = require('archiver'); const fs = require('fs-extra'); /** * 渲染 template 里面的所有文件 * * @param {Object} params 收集的用户输入字段 * @param {string} tmpStoreDir 临时文件夹存储路径 * @return {Promise} 渲染 promise */ function renderTemplate (params, tmpStoreDir) { let templateConfig = store.get('templateConfig'); let dirPath = params.dirPath || process.cwd(); // 模板文件渲染 let etplCompile = new etpl.Engine(templateConfig.etpl || conf.ETPL); // 把指定的开发者不需要的文件和文件夹都删掉 deleteFilter(tmpStoreDir, templateConfig.exportsIgnores); return new Promise((resolve, reject) => glob( '**/*', { // 要搜索的当前工作目录 cwd: tmpStoreDir, // 添加模式或glob模式数组以排除匹配。注意:无论其他设置如何,ignore模式始终处于dot:true模式状态。 ignore: (templateConfig.renderIgnores || []).concat(...conf.DEFAULT_RENDER_IGNORES) }, (err, files) => { files.forEach((file) => { // 文件路径 let filePath = path.resolve(tmpStoreDir, file); // 对象提供有关文件的信息。 // 如果 fs.Stats 对象描述常规文件,则返回 true。 if (fs.statSync(filePath).isFile()) { let content = fs.readFileSync(filePath, 'utf8'); // 这里可以直接通过外界配置的规则,重新计算出一份数据,只要和 template 里面的字段对应上就好了 let extDataTpls = templateConfig.extData || {}; let extData = {}; let commonData = conf.COMMON_DATA; Object.keys(extDataTpls).forEach((key) => { extData[key] = etplCompile.compile(`${extDataTpls[key]}`)(params); }); let renderData = Object.assign({}, params, extData, commonData); let afterCon = etplCompile.compile(content)(renderData); fs.writeFileSync(filePath, afterCon); } }); // addPackageJson(tmpStoreDir, params); if (params.isStream) { // 设置压缩级别 let archive = archiver('zip', { zlib: { level: 9 } }); let tmpZipPath = path.resolve(tmpStoreDir, '..', 'zip'); // 创建一个文件以将归档数据流式传输到。 let output = fs.createWriteStream(tmpZipPath); // 将 归档数据管道传输到文件 archiver.pipe(output); // 从子目录追加文件并在归档中命名为 params.name archive.directory(tmpStoreDir, params.name); // 完成归档(即我们已完成附加文件,但流必须完成) // 'close','end'或'finish'可能在调用此方法后立即触发,因此请事先注册 archive.finalize().on('finish', () => resolve(fs.createReadStream(tmpZipPath))); } else { fs.copySync(tmpStoreDir, dirPath); resolve(dirPath); } } )); } /** * 删除某个目录中的指定文件或文件夹 * * @param {string} dir 根目录 * @param {*} ignores 过滤的文件或文件夹数组 * @param {*} checkboxParams 需要插入的文件 */ function deleteFilter (dir, ignores = [], checkboxFile) { ignores.concat(...conf.DEFAULT_EXPORTS_IGNORES).forEach((target) => { let targetPath = path.resolve(dir, target); // 如果路径存在,则返回 true,否则返回 false。 // 删除文件 fs.existsSync(targetPath) && fs.removeSync(targetPath); }) }

2.然后在lib->scaffold->template.js中新增方法render渲染指定的模板模版

lib->scaffold->template.js

const locals = require('../../locals')(); /** * 渲染指定的模板模版 * * @param {Object} params 收集到的用户输入的参数 * @return {*} 导出的结果 */ exports.render = async function (params) { // 模板配置 let templateConfig = store.get('templateConfig') || await this.download(params); // 模板路径 let tmpStoreDir = path.resolve(conf.LOCAL_TEMPLATES_DIR, `${Date.now()}`); let storeDir = store.get('storeDir'); // 验证json let ajv = new Ajv({ allErrors: true }); let jsonSchema = schema.getMetaJsonSchema(templateConfig); jsonSchema.then(async (res) => { let validate = ajv.compile(res); let valid = validate(params); if (!valid) { throw new Error(JSON.stringify(validate.errors)); } try { // 如果路径存在,则返回 true,否则返回 false if (!fs.existsSync(storeDir)) { await this.download(params); } else { } // 将创建的目录路径 fs.mkdirSync(tmpStoreDir); // 拷贝文件 fs.copySync(storeDir, tmpStoreDir); // 渲染 template 里面的所有文件 let renderResult = await renderTemplate(params, tmpStoreDir); // 删除文件 fs.removeSync(tmpStoreDir); return renderResult; } catch (e) { throw new Error(locals.RENDER_TEMPLATE_ERROR); } }); }

3.然后在> lib->scaffold->index.js中管理template的render方法

新增extendsDefaultFields方法用于导出所有的模板文件

lib->scaffold->index.js

/** * 获取导出的所有的 fields (包含 default 参数) * * @param {Object} fields 传入的 fields * @param {Obejct} templateConf 模版的配置 * @return {Object} 输出的 fields */ async function extendsDefaultFields (fields = {}, templateConf = {}) { let defaultFields = {}; let schema = store.get('schema') || await Schema.getSchema(templateConf) Object.keys(schema).forEach((key) => (defaultFields[key] = schema[key].default)) /* eslint-disable fecs-use-computed-property */ // defaultFields.name = fields.name || 'ivue-cli' defaultFields.name = fields.name || 'ivue-cli'; defaultFields.dirPath = path.resolve(process.cwd(), fields.dirPath || '', defaultFields.name); return _.merge({}, defaultFields, fields); }

然后新增render方法用于通过指定的参数渲染下载成功的模板

lib->scaffold->index.js

/** * 通过指定的参数渲染下载成功的模板 * * @param {Object} params 导出参数 * @param {Object} templateConf 模版的配置 * @return {Promise} 导出的结果 */ exports.render = async function (params = {}, templateConf) { if (!templateConf) { // 如果实在没有提前下载模板,就现用默认的参数下载一个(这个模板是默认的) templateConf = await Schema.download(); } // 获取导出的所有的 fields params = await extendsDefaultFields(params, templateConf); return await template.render(params); }

4.我们再回到commander->scaffold->action.js对导出的文件进行逻辑判断

新增exportProject方法输出模板内容

commander->scaffold->action.js

/** * 输出项目 * * @param {Object} params 输出项目的参数 * @param {Object} templateConf 项目的配置内容 * @param {Object} checkboxParams 选框选择选项 */ async function exportProject (params, templateConf, checkboxParams) { let spinner = ora(locals.LOADING_EXPORT_PROJECT + '...'); spinner.start(); await scaffold.render(params, templateConf, checkboxParams); spinner.stop(); console.log(params) // for log beautify console.log(''); log.info(locals.INIT_SUCCESS); log.info(locals.INIT_NEXT_GUIDE + ':\n\n' + log.chalk.green('cd ' + params.name + '\n' + 'npm install\n' + 'npm run serve' )); try { await axios('https://lavas.baidu.com/api/logger/send?action=cli&commander=init'); } catch (e) { } }

调用exportProject方法导出所有的文件

commander->scaffold->action.js

// 当前执行node命令时候的文件夹地址 let cwd = process.cwd(); module.exports = async function (conf) { ..... // 第六步:渲染模板,并导出到指定的文件夹(当前文件夹) let projectTargetPath = path.resolve(params.dirPath || cwd, params.name); params = Object.assign({}, metaParams, params); // 测试某个路径下的文件是否存在 let isPathExist = await fs.pathExists(projectTargetPath); if (isPathExist) { // 错误提示项目已存在,避免覆盖原有项目 console.log(symbols.error, chalk.red('项目已存在')); return; } else { // 导出文件 await exportProject(params, templateConf, checkboxParams); } ..... }

以上也正是 ivue-cli 脚手架的全部源码。

到了这里也算的讲解完成了,如果需要更相信的代码同学们可以自行查看仓库源码去阅读,所有源码已标注了注释。

最后

新的一年终于吧这篇文章写完了,算是腻补上一年的遗憾~~😂😂😂

由于篇幅有点长,如有错误欢迎提出 issues 或者 star。

🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆

新的一年也要加油呀💪💪💪💪💪💪



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3